/*
 * Copyright (c) 2009-2013 EvTech Project
 * All rights reserved.
 * 
 * This file is part of Game packet.
 *
 * Game packet is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or 
 * (at your option) any later version.
 *
 * Game packet is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Game packet.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */


package fi.honkalampisaatio.sound;


import java.util.ArrayList;
import java.util.Hashtable;

import javax.sound.midi.Instrument;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Soundbank;
import javax.sound.midi.Synthesizer;



public class MidiPlayer {

	public static MidiPlayer INSTANCE;
	
	private static final String[]		sm_astrKeyNames = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};

	public enum DRUMS {ACOUSTIC_BASS_DRUM (35), 
					ACOUSTIC_SNARE (38),
					CLOSED_HI_HAT (42),
					COWBELL (56),
					OPEN_TRIANGLE (81);
		private int value;
		
		private DRUMS(int drumValue) {
			value = drumValue;
		}
		
		public int getDrumValue() {
			return value;
		}
	};
					
	
	/**
	 * Time for playing note in milliseconds
	 */
	private static final long WAIT_TIME = 1000;

	/**
	 * Midi channel for normal keyboard
	 */
	private MidiChannel motionChannel;
	
	/**
	 * Midi channel for drums
	 */
	private MidiChannel drumChannel;

	/**
	 * Midi channel for ekeys
	 */
	private MidiChannel ekeysChannel;

	/**
	 * Midi channel for normal keyboard
	 */
	private MidiChannel keysChannel;
	
	/**
	 * A Synthesizer generates sound. This usually happens when one of 
	 * the Synthesizer's MidiChannel objects receives a noteOn message, 
	 * either directly or via the Synthesizer object. Many Synthesizers 
	 * support Receivers, through which MIDI events can be delivered to 
	 * the Synthesizer. In such cases, the Synthesizer typically responds 
	 * by sending a corresponding message to the appropriate MidiChannel, 
	 * or by processing the event itself if the event isn't one of the 
	 * MIDI channel messages. 
	 */
	private Synthesizer  synth;
	
	/**
	 * Current instrument name for motion music
	 */
	private String currentInstrumentName;
	/**
	 * Current instrument name for midi keyboard
	 */
	private String currentMidiInstrumentName;
	/**
	 * Current instrument name for normal keyboard
	 */
	private String currentKeyboardInstrumentName;
	
	/**
	 * Running PlayNote threads
	 */
	private Hashtable<Integer, PlayNote> runningNoteThreads;
	private Hashtable<Integer, PlayNote> runningDrumThreads;

	private NameChangeListener listener;

	private NoteListener noteListener;
	

	public static MidiPlayer getInstance() throws MidiUnavailableException {
		if (INSTANCE == null) {
			INSTANCE = new MidiPlayer();
		}
		return INSTANCE;
	}
	
	/**
	 * Constructor
	 */
	public MidiPlayer() {
		 synth = null;
		 runningNoteThreads = new Hashtable<Integer, PlayNote>();
		 runningDrumThreads = new Hashtable<Integer, PlayNote>();
		 init();
	}

	@Override
	protected void finalize() throws Throwable {
		if(synth != null)
			synth.close();
		super.finalize();
	}

	/**
	 * Initializes synthesizer
	 * 
	 * @return if succeeded
	 */
	public boolean init() {
		boolean success = false;
		try {
			synth = MidiSystem.getSynthesizer();
			synth.open();
			motionChannel = synth.getChannels()[0];
			keysChannel = synth.getChannels()[1];
			ekeysChannel = synth.getChannels()[2];
			drumChannel = synth.getChannels()[9]; //drum channel
			Soundbank sb = synth.getDefaultSoundbank();
			if(sb!=null) {
				synth.loadAllInstruments(sb);
				setCurrentInstrument(0);
				setCurrentMidiInstrument(0);
				setCurrentKeyboardInstrument(0);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return success;
	}

	/**
	 * Gets loaded instrument names
	 */
	public ArrayList<String> getLoadedInstruments() {
		Instrument[] instruments = synth.getLoadedInstruments();
		ArrayList<String> list = new ArrayList<String>();
		
		for(int i=0; i<instruments.length;i++) {
			list.add(instruments[i].getName());
		}
		return list;
	}
	
	/**
	 * Gets current instrument name
	 */
	public String getCurrentInstrumentName() {
		return currentInstrumentName;
	}

	/**
	 * Gets current instrument index
	 */
	public int getCurrentInstrument() {
		return motionChannel.getProgram();
	}

	/**
	 * Sets current instrument name
	 */
	public int setCurrentInstrument(int i) {
		Instrument[] instruments = synth.getLoadedInstruments();
		motionChannel.programChange(i);
		currentInstrumentName=instruments[motionChannel.getProgram()].getName();
		NameChangeEvent nce = new NameChangeEvent(this, "Motion", currentInstrumentName);
		this.fireInstrumentChangeEvent(nce);
		return getCurrentInstrument();
	}

	public String getCurrentKeyboardInstrumentName() {
		return currentKeyboardInstrumentName;
	}

	/**
	 * Gets current instrument index
	 */
	public int getCurrentKeyboardInstrument() {
		return keysChannel.getProgram();
	}

	public int setCurrentKeyboardInstrument(int i) {
		Instrument[] instruments = synth.getLoadedInstruments();
		keysChannel.programChange(instruments[i].getPatch().getProgram());
		System.out.println("old KeyboardInstrument " + currentKeyboardInstrumentName);
		currentKeyboardInstrumentName=instruments[keysChannel.getProgram()].getName();
		NameChangeEvent nce = new NameChangeEvent(this, "Keyboard", currentKeyboardInstrumentName);
		this.fireInstrumentChangeEvent(nce);
		return getCurrentKeyboardInstrument();
		
	}

	public int setCurrentMidiInstrument(int i) {
		Instrument[] instruments = synth.getLoadedInstruments();
		ekeysChannel.programChange(instruments[i].getPatch().getProgram());
		currentMidiInstrumentName=instruments[ekeysChannel.getProgram()].getName();
		NameChangeEvent nce = new NameChangeEvent(this, "Midi", currentMidiInstrumentName);
		this.fireInstrumentChangeEvent(nce);
		return getCurrentMidiInstrument();
	}

	public int getCurrentMidiInstrument() {
		return ekeysChannel.getProgram();
	}

	public String getCurrentMidiInstrumentName() {
		return currentMidiInstrumentName;
	}

	
	public void playDrum(int drumValue) {
		PlayNote playNote = new PlayNote(drumChannel, drumValue, 125);
		PlayNote old = runningDrumThreads.put(Integer.valueOf(drumValue), playNote);
		if(old!=null) {
			old.stop();
		}
		Thread t = new Thread(playNote);
		t.start();
	}

	public void hitDrum(int drumValue,  int velocity) {
		if(drumChannel!=null) {
			drumChannel.noteOn(drumValue, velocity);
		}
	}

	public void playEkeys(int noteValue, int velocity) {
		if(ekeysChannel!=null) {
			ekeysChannel.noteOn(noteValue, velocity);
			NoteEvent ne = new NoteEvent(this, new MidiElement(
					currentMidiInstrumentName, noteValue, velocity, 
					System.currentTimeMillis()));
			this.fireNoteEvent(ne);
		}
	}

	public void playKeys(int noteValue, int velocity) {
		if(keysChannel!=null) {
			keysChannel.noteOn(noteValue, velocity);
		}
	}
	
	public void play(int noteValue, int velocity) {
		PlayNote playNote = new PlayNote(motionChannel, noteValue, velocity);
		PlayNote old = runningNoteThreads.put(Integer.valueOf(noteValue), playNote);
		if(old!=null) {
			old.stop();
		}
		Thread t = new Thread(playNote);
		t.start();
	}


	public static String getKeyName(int nKeyNumber)
	{
		if (nKeyNumber > 127)
		{
			return "illegal value";
		}
		else
		{
			int	nNote = nKeyNumber % 12;
			int	nOctave = nKeyNumber / 12;
			return sm_astrKeyNames[nNote] + (nOctave - 1);
		}
	}

	public void addInstrumentChangeListener(NameChangeListener  listener) {
		this.listener = listener;
	}
	
	public void removeInstrumentChangeListener(NameChangeListener listener) {
		if (this.listener == listener) {
			this.listener = null;
		}
	}
	
	private void fireInstrumentChangeEvent(NameChangeEvent nce) {
		if (this.listener != null)
			this.listener.changeEventOccurred(nce);
	}


	public void addNoteListener(NoteListener  listener) {
		this.noteListener = listener;
	}
	
	public void removeNoteListener(NoteListener listener) {
		if (this.noteListener == listener) {
			this.noteListener = null;
		}
	}
	
	private void fireNoteEvent(NoteEvent ne) {
		if (this.noteListener != null)
			this.noteListener.noteEventOccurred(ne);
	}
	
	
	/**
	 * Plays note in one second
	 */ 
	private class PlayNote implements Runnable {
		
		protected MidiChannel channel;
		/**
		 * Midi value of a note
		 */
		protected int midiValue;

		/**
		 * Velocity of a note
		 */
		protected int velocity;

		private boolean bContinue;
		
		/**
		 * Constructor
		 * 
		 * @param noteNumber midi value of a note
		 */
		public PlayNote(MidiChannel channel, int noteNumber, int velocity) {
			this.midiValue = noteNumber;
			this.velocity = velocity;
			bContinue = true;
			this.channel = channel;
		}
		
		public void stop() {
			bContinue = false;
			
		}

		@Override
		public void run() {
			// Middle C is 60, 
			// moderately loud is velocity 93.
			if(channel!=null) {
				try {
					Thread.sleep(WAIT_TIME/10);
					channel.noteOn(midiValue, velocity);
					long time = 0;
					while(time<WAIT_TIME && bContinue) {
						Thread.sleep(WAIT_TIME/10);
						time = time + WAIT_TIME/10;
					} 
				} catch (Exception e1) {
					e1.printStackTrace();
				} finally {
					channel.noteOff(midiValue);
				}
			}
		}
		
	}
	


}
